﻿using System.Collections.Generic;
using Microsoft.Xna.Framework;
using PoolGame2.Controls;
using PoolGame2.Game;
using PoolGame2.Managers;
using PoolGame2.Rules;

namespace PoolGame2.Physics
{
    public class PhysicsRuntime : IGameControlListener
    {
        
        public PhysicsState currentPhysicsState;
        public GameObject table;
        public CameraManager cameraManager;
        public PoolGame game;
        public GameRules rules;
        public List<RuleC.RuleTasks> tasks = new List<RuleC.RuleTasks>();
        public bool showDebug = false;
        public bool showTracking = false;
        public bool runOnce = false;
        public bool collideOnce = false;

        private PhysicsDebug debug;
        
        public PhysicsRuntime(Cue cue, GameObject table, PoolGame game, CameraManager cameraManager)
        {
            this.table = table;
            this.cameraManager = cameraManager;
            this.game = game;
            this.rules = new GameRules(game);
            currentPhysicsState = new PhysicsState(cue);
            this.debug = new PhysicsDebug(this);
            //debug.InitializeDebugDrawingElements();
            PoolGame.gameControls.addListener(this.debug);
        }

        public PhysicsState getState()
        {
            return currentPhysicsState;
        }

        public void doPhysics(float time, GameObject root)
        {
            float physicsResolution = time / GameConstants.PHYSICS_RESOLUTION;
            for (int i = 0; i < GameConstants.PHYSICS_RESOLUTION; i++)
            {
                DoPhysics(physicsResolution, root);
            }
        }

        // run the physics engine for this frame
        private void DoPhysics(float  timeInterval, GameObject root)
        {
            bool loop = true;
            while (loop)
            {
                float time = DoCollisions(timeInterval, false);
                if (time == 0.0f)
                {
                    loop = false;
                    this.DoIntegration(timeInterval, false);
                }
                else
                {
                    timeInterval = timeInterval - time;
                }
            }
            
            EnsureNoInterpenetration();

            if (showDebug)
            {
                debug.updateBallPositions();
            }
        }
        
        public bool AreBallsMoving()
        {
            foreach (Ball otherBall in currentPhysicsState.getBalls())
            {
                if (otherBall.velocity.Length() > 0.1f || otherBall.velocity.Y < -0.05f)
                    return true;
            }
            return false;
        }

        public bool IsAreaEmpty(Vector3 pos, out Ball hitBall )
        {
            return IsAreaEmpty(null, pos, out hitBall);
        }

        public bool IsAreaEmpty(Ball ignore, Vector3 pos, out Ball hitBall)
        {
            hitBall = null;
            foreach (Ball otherBall in currentPhysicsState.getBalls())
            {
                if (otherBall == ignore)
                    continue;

                if (Vector3.Distance(pos, otherBall.getPosition()) <= ((PhysicsConstants.DEFAULT_RADIUS + PhysicsConstants.DEFAULT_RADIUS) * 1.1f))
                {
                    hitBall = otherBall;
                    return false;
                }
            }
            return true;
        }

        public bool CanMoveObjectTo(Ball b, Vector3 pos )
        {
            foreach (Ball otherBall in currentPhysicsState.getBalls())
            {
                if (b.Equals(otherBall) == false && Vector3.DistanceSquared(pos, otherBall.getPosition()) < (PhysicsConstants.DEFAULT_RADIUS_SQUARED + PhysicsConstants.DEFAULT_RADIUS_SQUARED))
                {
                    return false;
                }
            }
            b.setPosition(pos);
            return true;
        }

        public void RegisterForCollisionDetection(Pocket p)
        {
            currentPhysicsState.AddPocket(p);
            //debug.InitializeDebugDrawingElements();
        }

        public void RegisterForCollisionDetection(Ball b)
        {
            currentPhysicsState.AddBall(b);
            //debug.InitializeDebugDrawingElements();
        }

        public void RegisterForCollisionDetection(Cushion c)
        {
            currentPhysicsState.AddCushion(c);
            //debug.InitializeDebugDrawingElements();
        }

        // recursive method to handle the game objects
        private void processObject(GameObject obj, float frameTime) {
            if (obj.getChildCount() > 0)
            {
                for (int i = 0; i < obj.getChildCount(); i++)
                {
                    processObject(obj.getChild(i), frameTime);
                }
            }
            else if (obj.getPhysicsObject() != null) 
            {
                moveObject(obj.getPhysicsObject(), frameTime);
            }
        }
        
        // move the object
        private void moveObject(PhysicsObject physicsObject, float frameTime)
        {
            if (physicsObject is Ball)
            {
                Ball b = (Ball)physicsObject;
                b.Integrate(frameTime, false);
            }
            else
            {
                physicsObject.Integrate(frameTime, false);
            }
        }

        private void DoIntegration(float time, bool forTracking)
        {
            foreach (Ball b in currentPhysicsState.getBalls())
            {
                b.Integrate(time, forTracking);
            }
            
        }

        private Collision GetEarliestCollision(Collision a, Collision b)
        {
            if (a == null && b == null) 
            {
                return null;
            } 
            else if (a == null) 
            {
                return b;
            } 
            else if (b == null) 
            {
                return a;
            }
            else if (a.collisionTime < b.collisionTime)
            {
                return a;
            }
            else
            {
                return b;
            }
        }

        private float DoCollisions(float time, bool recordCollisionsForTracking)
        {
            Collision firstCollision = null;
            foreach (Ball ball in currentPhysicsState.getBalls())
            {
                if (ball.getVisible())
                {
                    // cushion ball collisions
                    foreach (Cushion cush in currentPhysicsState.getCushions())
                    {
                        Collision collision = ball.CollidingWith(cush, time);
                        // set the collision time to the end of the frame
                        if (collision != null)
                        {
                            firstCollision = this.GetEarliestCollision(firstCollision, collision);
                            // Don't care about ball-cushion for the rules
                        }
                    }

                    // ball pocket collisions
                    foreach (Pocket pocket in currentPhysicsState.getPockets())
                    {
                        Collision collision = ball.CollidingWith(pocket, time);
                        if (collision != null)
                        {
                            // This code was never run because the pocket thing always returned null
                            // I commented on the firstCollision bit as a result - FIN
                            tasks.Add(rules.runRules(ball));
                            firstCollision = this.GetEarliestCollision(firstCollision, collision);
                            //Call the rules cos there's been a pocket-ball collision!
                        }
                    }
                    // ball ball collisions
                    foreach (Ball otherBall in currentPhysicsState.getBalls())
                    {
                        if (otherBall.getNumber() != ball.getNumber())
                        {
                            Collision collision = ball.CollidingWith(otherBall, time);
                            if (collision != null)
                            {
                                firstCollision = this.GetEarliestCollision(firstCollision, collision);
                                //Ball-ball collision!
                                if (ball.getNumber() == 0 && runOnce == false)
                                {
                                    runOnce = true;
                                    collideOnce = true;
                                    tasks.Add(rules.runRules(ball, otherBall));
                                }
                                else if (otherBall.getNumber() == 0 && runOnce == false)
                                {
                                    runOnce = true;
                                    collideOnce = true;
                                    tasks.Add(rules.runRules(otherBall, ball));
                                }
                            }
                        }
                    }
                }
            }
            if (firstCollision != null)
            {
                // Integrate forward to the collision time
                this.DoIntegration(firstCollision.collisionTime, recordCollisionsForTracking);
                // then calculate the collision response
                if (recordCollisionsForTracking)
                {
                    AddTrackingPoint(firstCollision);
                }
                firstCollision.ResolveCollision(!recordCollisionsForTracking);
                return firstCollision.collisionTime;
            }
            else
            {
                return 0.0f;
            }
        }

        public static Vector3 PointEdgeClosestPoint(Vector3 point, Vector3 segment_a, Vector3 segment_b)
        {
            Vector3 ab = segment_b - segment_a;
            float t = Vector3.Dot(point - segment_a, ab) / Vector3.Dot(ab, ab);
            if (t < 0.0f)
            {
                t = 0.0f;
            }
            if (t > 1.0f)
            {
                t = 1.0f;
            }
            return segment_a + t * ab;
        }

        public void Draw(GameTime gameTime)
        {
            if (showDebug)
            {
                debug.DrawDebug(gameTime);
            }

            if (showTracking)
            {
                //RecordTrackingPoints();
                debug.DrawTrackingLines(gameTime);
            }

        }

        public List<RuleC.RuleTasks> getTasks()
        {
            return tasks;
        }

        public void resetTasks()
        {
            tasks = new List<RuleC.RuleTasks>();
            runOnce = false;
            collideOnce = false;
        }

        private void EnsureNoInterpenetration()
        {
            foreach (Ball ball1 in currentPhysicsState.getBalls())
            {
                foreach (Ball ball2 in currentPhysicsState.getBalls())
                {
                    if (ball1.getNumber() != ball2.getNumber())
                    {
                        float distanceSquared = Vector3.DistanceSquared(ball1.getPosition(), ball2.getPosition());
                        float diff = PhysicsConstants.DEFAULT_RADIUS_SQUARED * 2 - distanceSquared;
                        if (diff > 0)
                        {
                            ball1.setPosition(ball1.getPosition() - ball1.getVelocity() * diff);
                            ball2.setPosition(ball2.getPosition() - ball2.getVelocity() * diff);
                        }
                    }
                }
            }
        }

        public int getColours()
        {
            int colours = 0;
            foreach (Ball curBall in currentPhysicsState.getBalls())
            {
                if (curBall.getPocketed())
                {
                    if (curBall.getNumber() < 8)
                    {
                        colours++;
                    }
                }
            }
            return colours;
        }

        public int getStripes()
        {
            int stripes = 0;
            foreach (Ball curBall in currentPhysicsState.getBalls())
            {
                if (curBall.getPocketed())
                {
                    if (curBall.getNumber() > 8)
                    {
                        stripes++;
                    }
                }
            }
            return stripes;
        }

        private void RecordTrackingPoints()
        {
            // clear the tracking points list
            trackingPoints.Clear();

            trackingPoints.Add(currentPhysicsState.GetCueball().getPosition());

            // set the cue ball as the last hit ball
            trackingBall = 0;
                
            // save the old state
            currentPhysicsState.RecordOldBallPositions();

            // simulate a strike on the cueball in its current orientation.
            Vector3 powerAndDirection = currentPhysicsState.getCue().GetUnitVectorDirection() * 400;
            Vector3 cueTipPosition = currentPhysicsState.getCue().GetCueTipPosition();
            currentPhysicsState.GetCueball().Strike(powerAndDirection, cueTipPosition);

            // integrate forward for a fixed amount of time, recording the collisions
            float timeInterval = PhysicsConstants.TRACK_FORWARD_TIME;
            int collisionCount = 0;

            // run the physics engine as per normal - however stop after x collisions
            bool loop = true;
            while (loop && collisionCount < 8)
            {
                // capture the collision points and order
                float time = DoCollisions(timeInterval, true);
                if (time == 0.0f)
                {
                    loop = false;
                    this.DoIntegration(timeInterval, true);
                }
                else
                {
                    timeInterval = timeInterval - time;
                    collisionCount++;
                }
            }
            EnsureNoInterpenetration();

            // reinstate the old state
            currentPhysicsState.RestoreBallPositions();
            debug.SetTrackingPoints(trackingPoints);
        }

        private List<Vector3> trackingPoints = new List<Vector3>();

        public void GameControlPerformed(List<GameControlAction> actions)
        {
            foreach (GameControlAction action in actions)
            {
                ControlActions act = action.GetAction();
                if (act == ControlActions.SWITCH_TRACKING)
                {
                    showTracking = !showTracking;
                    if (showTracking) 
                    { 
                        RecordTrackingPoints(); 
                    }
                }
                else if (act == ControlActions.DEBUG_SWITCH)
                {
                    showDebug = !showDebug;
                }
                else if (act == ControlActions.CUE_MOVEMENT)
                {
                    if (showTracking)
                    {
                        RecordTrackingPoints();
                    }

                }
            }
        }

        private void AddTrackingPoint(Collision collision) {

            // if it a ball cushion collision then just add the point to the list if it is the active ball

            // for ball - ball collision we test if the collision contains the ball we are interested in

            if (collision is BallCushionCollision) {
                Ball a = (Ball)collision.a;
                if (a.getNumber() == trackingBall)
                {
                    trackingPoints.Add(collision.GetCollisionPoint());
                }
            }
            else if (collision is BallPocketCollision)
            {
                Ball a = (Ball)collision.a;
                if (a.getNumber() == trackingBall)
                {
                    trackingPoints.Add(collision.GetCollisionPoint());
                }
            }
            else if (collision is BallBallCollision)
            {
                Ball a = (Ball)collision.a;
                Ball b = (Ball)collision.b;
                if (a.getNumber() == trackingBall)
                {
                    trackingPoints.Add(collision.GetCollisionPoint());
                    trackingBall = b.getNumber();
                }
                else if (b.getNumber() == trackingBall)
                {
                    trackingPoints.Add(collision.GetCollisionPoint());
                    trackingBall = a.getNumber();
                }
            }
        }

        private int trackingBall;

    }
}
